﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Diagnostics;

namespace EngineOfEvermore.Common
{

    // TODO: How to handle collections?
    class XmlMerger
    {
        private static XNodeEqualityComparer _comparer = new XNodeEqualityComparer();

        // This method assumes that instance always is a superset of template
        public XElement Diff( XElement template, XElement instance )
        {
            Debug.Assert( template.Name == instance.Name );
            
            var diff = new XElement( instance.Name );

            _copyAttributesIfDifferent( template, instance, diff );

            // If the element has no children, copy the content
            if ( !instance.HasElements )
            {
                Debug.Assert( !template.HasElements );

                _copyElementValueIfDifferent( template, instance, diff );
            }
            else
            {
                // Else copy the children
                foreach ( var instanceChild in instance.Elements() )
                {
                    _copyChildIfDifferent( template, diff, instanceChild );
                }
            }

            if ( !diff.HasElements && !diff.HasAttributes && String.IsNullOrEmpty( diff.Value ) )
                return null;
            
            return diff;
        }

        private void _copyChildIfDifferent( XElement template, XElement diff, XElement child )
        {
            XElement eq = _findEqual( child, template );

            // If this is a whole new node, just clone it into the diff
            if ( eq == null )
            {
                diff.Add( new XElement( child ) );
            }
            else
            {
                var diffChild = this.Diff( eq, child );

                if ( diffChild != null )
                {
                    diff.Add( diffChild );
                }
            }
        }

        private static void _copyElementValueIfDifferent( XElement template, XElement instance, XElement diff )
        {
            if ( instance.Value != template.Value )
            {
                diff.SetValue( instance.Value );
            }
        }

        private void _copyAttributesIfDifferent( XElement template, XElement instance,
            XElement diff )
        {
            foreach ( var attribute in instance.Attributes() )
            {
                var templateAttribute = template.Attribute( attribute.Name );

                if ( templateAttribute == null )
                {
                    diff.SetAttributeValue( attribute.Name, attribute.Value );
                }
                else
                {
                    if ( attribute.Value != templateAttribute.Value )
                    {
                        diff.SetAttributeValue( attribute.Name, attribute.Value );
                    }
                }
            }
        }

        private XElement _findEqual( XElement child, XElement otherParent )
        {
            var itemsWithEqualName = otherParent.Elements( child.Name );

            var count = itemsWithEqualName.Count();

            if ( count == 0 )
            {
                return null;
            }

            return itemsWithEqualName.Single();
        }
    }

    namespace Test
    {
        using NUnit.Framework;
        using System.Xml;

        [TestFixture]
        public class XmlMergerFixture
        {
            private XmlMerger _merger = new XmlMerger();
            private XNodeEqualityComparer _comparer = new XNodeEqualityComparer();

            [Test]
            public void TestSimpleDiff()
            {
                XElement template =
                    new XElement( "SceneObject",
                        new XElement( "Position", "0 1" ),
                        new XElement( "Rotation", "0" ) 
                        );

                XElement instance =
                    new XElement( "SceneObject",
                        new XElement( "Position", "0 1" ),
                        new XElement( "Rotation", "1" )
                        );

                XElement diff = _merger.Diff( template, instance );

                Assert.That( diff.Name.LocalName, Is.EqualTo( "SceneObject" ) );
                Assert.That( diff.Elements( "Position" ), Is.Empty );
                Assert.That( diff.Elements( "Rotation" ).Count() == 1 );
                Assert.That( diff.Elements( "Rotation" ).Single().Value, Is.EqualTo( "1" ) );
            }

            [Test]
            public void TestSimpleDiffWithAttributes()
            {
                XElement template =
                    new XElement( "SceneObject",
                        new XElement( "Position", 
                            new XAttribute( "value", "0 0" ) ),
                        new XElement( "Rotation", "0" )
                        );

                XElement instance =
                    new XElement( "SceneObject",
                        new XElement( "Position",
                            new XAttribute( "value", "1 0" ) ),
                        new XElement( "Rotation", "0" )
                        );

                XElement diff = _merger.Diff( template, instance );

                Assert.That( diff.Name.LocalName, Is.EqualTo( "SceneObject" ) );
                Assert.That( diff.Elements( "Position" ).Count(), Is.EqualTo( 1 ) );
                Assert.That( diff.Elements( "Position" ).Single().Attribute( "value" ), 
                    Is.Not.Null );
                Assert.That( diff.Elements( "Position" ).Single().Attribute( "value" ).Value,
                    Is.EqualTo( "1 0" ) );
                Assert.That( diff.Elements( "Rotation" ), Is.Empty );
            }

            [Test]
            public void TestSimpleDiffWithAdditionalAttributes()
            {
                XElement template =
                    new XElement( "SceneObject",
                        new XElement( "Position",
                            new XAttribute( "value", "0 0" ) 
                            ),
                        new XElement( "Rotation", "0" )
                        );

                XElement instance =
                    new XElement( "SceneObject",
                        new XElement( "Position",
                            new XAttribute( "value", "1 0" ),
                            new XAttribute( "value2", "0 0" )
                            ),
                        new XElement( "Rotation", "0" )
                        );

                XElement diff = _merger.Diff( template, instance );

                Assert.That( diff.Name.LocalName, Is.EqualTo( "SceneObject" ) );
                Assert.That( diff.Elements( "Position" ).Count(), Is.EqualTo( 1 ) );
                Assert.That( diff.Elements( "Position" ).Single().Attribute( "value" ),
                    Is.Not.Null );
                Assert.That( diff.Elements( "Position" ).Single().Attribute( "value" ).Value,
                    Is.EqualTo( "1 0" ) );

                Assert.That( diff.Elements( "Position" ).Single().Attribute( "value2" ),
                    Is.Not.Null );
                Assert.That( diff.Elements( "Position" ).Single().Attribute( "value2" ).Value,
                    Is.EqualTo( "0 0" ) );

                Assert.That( diff.Elements( "Rotation" ), Is.Empty );
            }

            [Test]
            public void TestSimpleDiffWithAdditionalElements()
            {
                XElement template =
                    new XElement( "SceneObject",
                        new XElement( "Position", "0 1" ),
                        new XElement( "Rotation", "0" )
                        );

                XElement instance =
                    new XElement( "SceneObject",
                        new XElement( "Position", "0 1" ),
                        new XElement( "Rotation", "0" ),
                        new XElement( "Velocity", "1 1" )
                        );

                XElement diff = _merger.Diff( template, instance );

                Assert.That( diff.Name.LocalName, Is.EqualTo( "SceneObject" ) );
                Assert.That( diff.Elements( "Position" ), Is.Empty );
                Assert.That( diff.Elements( "Rotation" ), Is.Empty );

                Assert.That( diff.Elements( "Velocity" ).Count(), Is.EqualTo( 1 ) );
                Assert.That( diff.Elements( "Velocity" ).Single().Value, Is.EqualTo( "1 1" ) );
            }

            [Test]
            public void TestClone()
            {
                var e =
                    new XElement( "Parent",
                        new XAttribute( "attribute1", "1" ),
                        new XAttribute( "attribute2", "2" ),
                        new XElement( "Child1",
                            new XAttribute( "attributeX", "X" ),
                            new XElement( "GrandChild", "GrandChildContent" )
                            ),
                        new XElement( "Child2", "Child2Content" )
                        );

                var clone = new XElement( e );

                Assert.That( e.Name, Is.EqualTo( clone.Name ) );
                Assert.That( e.Nodes().Count(), Is.EqualTo( clone.Nodes().Count() ) );
                Assert.That( clone.Attribute( "attribute1" ), Is.Not.Null );
                Assert.That( clone.Attribute( "attribute2" ), Is.Not.Null );
                Assert.That( clone.Attribute( "attribute1" ).Value, Is.EqualTo( "1" ) );
                Assert.That( clone.Attribute( "attribute2" ).Value, Is.EqualTo( "2" ) );
                
                Assert.That( clone.Element( "Child1" ), Is.Not.Null );
                Assert.That( clone.Element( "Child1" ).Attribute( "attributeX" ), Is.Not.Null );
                Assert.That( clone.Element( "Child1" ).Attribute( "attributeX" ).Value, 
                    Is.EqualTo( "X" ) );

                Assert.That( clone.Element( "Child1" ).Element( "GrandChild" ), Is.Not.Null );
                Assert.That( clone.Element( "Child1" ).Element( "GrandChild" ).Value,
                    Is.EqualTo( "GrandChildContent" ) );

                Assert.That( clone.Element( "Child2" ), Is.Not.Null );
                Assert.That( clone.Element( "Child2" ).Value, Is.EqualTo( "Child2Content" ) );
            }

            [Test]
            public void TestRecursiveDiff()
            {
                var template =
                    new XElement( "Parent",
                        new XAttribute( "attribute1", "1" ),
                        new XAttribute( "attribute2", "2" ),
                        new XElement( "Child1",
                            new XAttribute( "attributeX", "X" ),
                            new XElement( "GrandChild1", "GrandChildContent1" )
                            ),
                        new XElement( "Child2", "Child2Content" )
                        );

                var instance =
                    new XElement( "Parent",
                        new XAttribute( "attribute1", "1" ),
                        new XAttribute( "attribute2", "2A" ),
                        new XAttribute( "attribute3", "3" ),
                        new XElement( "Child1",
                            new XAttribute( "attributeX", "X" ),
                            new XAttribute( "attributeY", "Y" ),
                            new XElement( "GrandChild1", "GrandChildContent1A" ),
                            new XElement( "GrandChild2", "GrandChildContent2" )
                            ),
                        new XElement( "Child2", "Child2Content" ),
                        new XElement( "Child3" )
                        );

                var expectedDiff =
                    new XElement( "Parent",
                        new XAttribute( "attribute2", "2A" ),
                        new XAttribute( "attribute3", "3" ),
                        new XElement( "Child1",
                            new XAttribute( "attributeY", "Y" ),
                            new XElement( "GrandChild1", "GrandChildContent1A" ),
                            new XElement( "GrandChild2", "GrandChildContent2" )
                            ),
                        new XElement( "Child3" )
                        );

                var diff = _merger.Diff( template, instance );

                Assert.That( _comparer.Equals( expectedDiff, diff ) );
            }

            [Test]
            public void TestDiffWithEmptyElement()
            {
                var template =
                    new XElement( "X" );

                var instance =
                    new XElement( "X",
                        new XElement( "Y" ),
                        new XAttribute( "A", "A" )
                        );

                var expectedDiff = instance;

                var diff = _merger.Diff( template, instance );
                
                Assert.That( _comparer.Equals( diff, expectedDiff ) );
            }

            private void PrintElement( XElement e )
            {
                XmlWriterSettings s = new XmlWriterSettings();
                s.Indent = true;
                s.OmitXmlDeclaration = true;

                var writer = XmlWriter.Create( System.Console.Out, s );

                e.WriteTo( writer );

                writer.Flush();
            }
        }
    
    }
}
